/*
 * Copyright (c) 2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import 'package:flutter/cupertino.dart';
import 'package:xml/xml.dart';

import '../common/test_page.dart';
import 'utils/assertions.dart';

class BuilderTestPage extends TestPage {
  BuilderTestPage(String title, {Key? key}) : super(title, key: key) {
    test('basic', () {
      final builder = XmlBuilder();
      builder.declaration(encoding: 'UTF-8');
      builder.processing('xml-stylesheet', 'href="/style.css" type="text/css" title="default stylesheet"');
      builder.element('bookstore', nest: () {
        builder.comment('Only one book?');
        builder.element('book', nest: () {
          builder.element('title', nest: () {
            builder.attribute('lang', 'en');
            builder.text('Harry ');
            builder.cdata('Potter');
          });
          builder.element('price', nest: 29.99);
        });
      });
      final document = builder.buildDocument();
      assertDocumentTreeInvariants(document);
      final actual = document.toString();
      const expected = '<?xml version="1.0" encoding="UTF-8"?>'
          '<?xml-stylesheet href="/style.css" type="text/css" title="default stylesheet"?>'
          '<bookstore>'
          '<!--Only one book?-->'
          '<book>'
          '<title lang="en">Harry <![CDATA[Potter]]></title>'
          '<price>29.99</price>'
          '</book>'
          '</bookstore>';
      expect(actual, expected);
    });
    test('all', () {
      final builder = XmlBuilder();
      builder.declaration();
      builder.doctype('note', systemId: 'Note.dtd');
      builder.processing('processing', 'instruction');
      builder.element('element1', attributes: {'attribute1': 'value1'}, nest: () {
        builder.attribute('attribute2', 'value2', attributeType: XmlAttributeType.DOUBLE_QUOTE);
        builder.attribute('attribute3', 'value3', attributeType: XmlAttributeType.SINGLE_QUOTE);
        builder.element('element2');
        builder.comment('comment');
        builder.cdata('cdata');
        builder.text('textual');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<?xml version="1.0"?>'
          '<!DOCTYPE note SYSTEM "Note.dtd">'
          '<?processing instruction?>'
          '<element1 attribute1="value1" attribute2="value2" '
          'attribute3=\'value3\'>'
          '<element2/>'
          '<!--comment-->'
          '<![CDATA[cdata]]>'
          'textual'
          '</element1>';
      expect(actual, expected);
    });
    test('self-closing', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.element('self-closing-default');
        builder.element('self-closing-true', isSelfClosing: true);
        builder.element('self-closing-true-with-children', isSelfClosing: true, nest: '!');
        builder.element('self-closing-false', isSelfClosing: false);
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element>'
          '<self-closing-default/>'
          '<self-closing-true/>'
          '<self-closing-true-with-children>!</self-closing-true-with-children>'
          '<self-closing-false></self-closing-false>'
          '</element>';
      expect(actual, expected);
    });
    test('nested string', () {
      final builder = XmlBuilder();
      builder.element('element', nest: 'string');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element>string</element>';
      expect(actual, expected);
    });
    test('nested iterable', () {
      final builder = XmlBuilder();
      builder.element('element', nest: [
        () => builder.text('st'),
        'ri',
        ['n', 'g']
      ]);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element>string</element>';
      expect(actual, expected);
    });
    test('nested node (element)', () {
      final builder = XmlBuilder();
      final nested = XmlElement(XmlName('nested'));
      builder.element('element', nest: nested);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
      expect(xml.children[0].children[0] != nested, null);
      final actual = xml.toString();
      const expected = '<element><nested/></element>';
      expect(actual, expected);
    });
    test('nested node (element, repeated)', () {
      final builder = XmlBuilder();
      final nested = XmlElement(XmlName('nested'));
      builder.element('element', nest: [nested, nested]);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
      expect(xml.children[0].children[0] != nested, null);
      expect(xml.children[0].children[1].toXmlString(), nested.toXmlString());
      expect(xml.children[0].children[1] != nested, null);
      final actual = xml.toString();
      const expected = '<element><nested/><nested/></element>';
      expect(actual, expected);
    });
    test('nested node (text)', () {
      final builder = XmlBuilder();
      final nested = XmlText('text');
      builder.element('element', nest: nested);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
      expect(xml.children[0].children[0] != nested, null);
      final actual = xml.toString();
      const expected = '<element>text</element>';
      expect(actual, expected);
    });
    test('nested node (text, repeated)', () {
      final builder = XmlBuilder();
      final nested = XmlText('text');
      builder.element('element', nest: [nested, nested]);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].children[0].text, 'texttext');
      expect(xml.children[0].children[0] != nested, null);
      final actual = xml.toString();
      const expected = '<element>texttext</element>';
      expect(actual, expected);
    });
    test('nested node (data)', () {
      final builder = XmlBuilder();
      final nested = XmlComment('abc');
      builder.element('element', nest: nested);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
      expect(xml.children[0].children[0] != nested, null);
      final actual = xml.toString();
      const expected = '<element><!--abc--></element>';
      expect(actual, expected);
    });
    test('nested node (attribute)', () {
      final builder = XmlBuilder();
      final nested = XmlAttribute(XmlName('foo'), 'bar');
      builder.element('element', nest: nested);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].attributes[0].toXmlString(), nested.toXmlString());
      expect(xml.children[0].attributes[0] != nested, null);
      final actual = xml.toString();
      const expected = '<element foo="bar"/>';
      expect(actual, expected);
    });
    test('nested node (document), 此处应该为X', () {
      final builder = XmlBuilder();
      final nested = XmlDocument([]);
      builder.element('element', nest: nested);
      // expect(() => builder.element('element', nest: nested), throwsArgumentError);
    });
    test('nested node (document fragment)', () {
      final builder = XmlBuilder();
      final nested = XmlDocumentFragment([XmlText('foo'), XmlComment('bar')]);
      builder.element('element', nest: nested);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      expect(xml.children[0].children[0].toXmlString(), nested.children[0].toXmlString());
      expect(xml.children[0].children[0] != (nested.children[0]), null);
      expect(xml.children[0].children[1].toXmlString(), nested.children[1].toXmlString());
      expect(xml.children[0].children[1] != (nested.children[1]), null);
      final actual = xml.toString();
      const expected = '<element>foo<!--bar--></element>';
      expect(actual, expected);
    });
    test('text', () {
      final builder = XmlBuilder();
      builder.element('text', nest: () {
        builder.text('abc');
        builder.text('');
        builder.text('def');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<text>abcdef</text>';
      expect(actual, expected);
    });
    test('doctype (plain)', () {
      final builder = XmlBuilder()
        ..doctype('note')
        ..element('root');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<!DOCTYPE note><root/>';
      expect(actual, expected);
    });
    test('doctype (system ID)', () {
      final builder = XmlBuilder()
        ..doctype('note', systemId: 'system.dtd')
        ..element('root');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<!DOCTYPE note SYSTEM "system.dtd"><root/>';
      expect(actual, expected);
    });
    test('doctype (public ID)', () {
      final builder = XmlBuilder()
        ..doctype('note', publicId: 'public.dtd', systemId: 'system.dtd')
        ..element('root');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<!DOCTYPE note PUBLIC "public.dtd" "system.dtd"><root/>';
      expect(actual, expected);
    });
    test('doctype (internal subset)', () {
      final builder = XmlBuilder()
        ..doctype('note', internalSubset: '<!ELEMENT br EMPTY>')
        ..element('root');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<!DOCTYPE note [<!ELEMENT br EMPTY>]><root/>';
      expect(actual, expected);
    });
    test('doctype (error), 此处应该为X', () {
      final builder = XmlBuilder();
      builder.doctype('note', publicId: 'public.dtd');
      // expect(() => builder.doctype('note', publicId: 'public.dtd'), throwsArgumentError);
    });
    test('attribute', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.attribute('foo', 'bar');
      });
      final xml = builder.buildDocument();
      assertDocumentInvariants(xml);
      final actual = xml.toString();
      expect(actual, '<element foo="bar"/>');
    });
    test('attribute (single quote)', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.attribute('foo', 'bar', attributeType: XmlAttributeType.SINGLE_QUOTE);
      });
      final xml = builder.buildDocument();
      assertDocumentInvariants(xml);
      final actual = xml.toString();
      expect(actual, '<element foo=\'bar\'/>');
    });
    test('attribute (replaced)', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.attribute('foo', 'bar');
        builder.attribute('foo', 'zork');
      });
      final xml = builder.buildDocument();
      assertDocumentInvariants(xml);
      final actual = xml.toString();
      expect(actual, '<element foo="zork"/>');
    });
    test('attribute (removed)', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.attribute('foo', 'bar');
        builder.attribute('foo', null);
      });
      final xml = builder.buildDocument();
      assertDocumentInvariants(xml);
      final actual = xml.toString();
      expect(actual, '<element/>');
    });
    test('attribute (multiple)', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        for (var i = 1; i <= 5; i++) {
          builder.attribute('a$i', i);
        }
      });
      final xml = builder.buildDocument();
      assertDocumentInvariants(xml);
      final actual = xml.toString();
      expect(actual, '<element a1="1" a2="2" a3="3" a4="4" a5="5"/>');
    });
    test('attribute (update)', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.attribute('lang', 'en');
        builder.attribute('lang', 'de');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element lang="de"/>';
      expect(actual, expected);
    });
    test('attribute (update with namespace)', () {
      const namespaces = {
        'http://www.w3.org/1999/xhtml': 'xhtml',
        'http://www.w3.org/1999/xlink': 'xlink',
      };
      final builder = XmlBuilder();
      builder.element('element', namespaces: namespaces, nest: () {
        for (var uri in namespaces.keys) {
          builder.attribute('lang', 'en', namespace: uri);
        }
        for (var uri in namespaces.keys) {
          builder.attribute('lang', 'de', namespace: uri);
        }
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element '
          'xmlns:xhtml="http://www.w3.org/1999/xhtml" '
          'xmlns:xlink="http://www.w3.org/1999/xlink" '
          'xhtml:lang="de" xlink:lang="de"/>';
      expect(actual, expected);
    });
    test('attribute (update with unknown namespace)', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.attribute('xhtml:lang', 'en');
        builder.attribute('xhtml:lang', 'de');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element xhtml:lang="de"/>';
      expect(actual, expected);
    });
    test('xml', () {
      final builder = XmlBuilder();
      builder.xml('<element attr="value"/>');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element attr="value"/>';
      expect(actual, expected);
    });
    test('xml (nested)', () {
      final builder = XmlBuilder();
      builder.element('outer', nest: () {
        builder.xml('<inner attr="value"/>');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<outer><inner attr="value"/></outer>';
      expect(actual, expected);
    });
    test('xml (multiple)', () {
      final builder = XmlBuilder();
      builder.element('outer', nest: () {
        builder.xml('<inner1/>');
        builder.xml('<inner2>hello</inner2>');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<outer><inner1/><inner2>hello</inner2></outer>';
      expect(actual, expected);
    });
    test('xml (invalid), 此处应该为X', () {
      final builder = XmlBuilder();
      builder.element('outer', nest: () {
        builder.xml('<broken>');
      });
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<outer/>';
      expect(actual, expected);
    });
    test('namespace binding', () {
      const uri = 'http://www.w3.org/2001/XMLSchema';
      final builder = XmlBuilder();
      builder.element('schema', nest: () {
        builder.namespace(uri, 'xsd');
        builder.attribute('lang', 'en', namespace: uri);
        builder.element('element', namespace: uri);
      }, namespace: uri);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsd:lang="en">'
          '<xsd:element/>'
          '</xsd:schema>';
      expect(actual, expected);
    });
    test('default namespace binding', () {
      const uri = 'http://www.w3.org/2001/XMLSchema';
      final builder = XmlBuilder();
      builder.element('schema', nest: () {
        builder.namespace(uri);
        builder.attribute('lang', 'en', namespace: uri);
        builder.element('element', namespace: uri);
      }, namespace: uri);
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<schema xmlns="http://www.w3.org/2001/XMLSchema" lang="en">'
          '<element/>'
          '</schema>';
      expect(actual, expected);
    });
    test('undefined namespace, 此处应该为X', () {
      final builder = XmlBuilder();
      builder.element('element', namespace: 'http://foo.com/');
      // expect(() => builder.element('element', namespace: 'http://foo.com/'), throwsArgumentError);
    });
    test('invalid namespace, 此处应该为X', () {
      final builder = XmlBuilder();
      builder.namespace('http://foo.com/', 'xml');
      builder.namespace('http://2.foo.com/', 'xmlns');
      builder.element('element', nest: () {
        // expect(() => builder.namespace('http://foo.com/', 'xml'), throwsArgumentError);
        // expect(() => builder.namespace('http://2.foo.com/', 'xmlns'), throwsArgumentError);
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element/>';
      expect(actual, expected);
    });
    test('conflicting namespace, 此处应该为X', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.namespace('http://2.foo.com/', 'foo');
        // expect(() => builder.namespace('http://2.foo.com/', 'foo'), throwsArgumentError);
      }, namespace: 'http://foo.com/');
      final actual = builder.buildDocument().toString();
      const expected = '<foo:element xmlns:foo="http://foo.com/"/>';
      expect(actual, expected);
    });
    test('unused namespace', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.namespace('http://foo.com/', 'foo');
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element xmlns:foo="http://foo.com/"/>';
      expect(actual, expected);
    });
    test('unused namespace (optimized)', () {
      final builder = XmlBuilder(optimizeNamespaces: true);
      builder.element('element', nest: () {
        builder.namespace('http://foo.com/', 'foo');
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element/>';
      expect(actual, expected);
    });
    test('duplicate namespace', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.element('outer', nest: () {
          builder.namespace('http://foo.com/', 'foo');
          builder.element('inner', nest: () {
            builder.namespace('http://foo.com/', 'foo');
            builder.attribute('lang', 'en', namespace: 'http://foo.com/');
          });
        });
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element xmlns:foo="http://foo.com/">'
          '<outer xmlns:foo="http://foo.com/">'
          '<inner xmlns:foo="http://foo.com/" foo:lang="en"/>'
          '</outer>'
          '</element>';
      expect(actual, expected);
    });
    test('duplicate namespace on attribute (optimized)', () {
      final builder = XmlBuilder(optimizeNamespaces: true);
      builder.element('element', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.element('outer', nest: () {
          builder.namespace('http://foo.com/', 'foo');
          builder.element('inner', nest: () {
            builder.namespace('http://foo.com/', 'foo');
            builder.attribute('lang', 'en', namespace: 'http://foo.com/');
          });
        });
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element xmlns:foo="http://foo.com/">'
          '<outer>'
          '<inner foo:lang="en"/>'
          '</outer>'
          '</element>';
      expect(actual, expected);
    });
    test('duplicate namespace on element (optimized)', () {
      final builder = XmlBuilder(optimizeNamespaces: true);
      builder.element('element', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.element('outer', nest: () {
          builder.namespace('http://foo.com/', 'foo');
          builder.element('inner', namespace: 'http://foo.com/');
        });
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element xmlns:foo="http://foo.com/">'
          '<outer>'
          '<foo:inner/>'
          '</outer>'
          '</element>';
      expect(actual, expected);
    });
    test('namespace defined with element', () {
      final builder = XmlBuilder();
      builder.element('element', namespaces: {
        'http://foo.com/': 'foo',
        'http://bar.com/': null,
      });
      final actual = builder.buildDocument().toString();
      const expected = '<element xmlns:foo="http://foo.com/" '
          'xmlns="http://bar.com/"/>';
      expect(actual, expected);
    });
    test('entities cdata escape', () {
      final builder = XmlBuilder();
      builder.element('element', nest: '<test><![CDATA[string]]></test>');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<element>&lt;test>&lt;![CDATA[string]]&gt;&lt;/test></element>';
      expect(actual, expected);
    });
    test('declaration', () {
      final builder = XmlBuilder();
      builder.declaration(version: '0.5', encoding: 'ASCII', attributes: {'foo': 'bar'});
      builder.element('data');
      final xml = builder.buildDocument();
      assertDocumentTreeInvariants(xml);
      final actual = xml.toString();
      const expected = '<?xml version="0.5" encoding="ASCII" foo="bar"?><data/>';
      expect(actual, expected);
    });
    test('declaration outside of document, 此处应该为X', () {
      final builder = XmlBuilder();
      builder.element('data', nest: builder.declaration);
      // expect(
      //     () => builder.element('data', nest: builder.declaration),
      //     throwsA(isXmlNodeTypeException(
      //         message: startsWith('Got XmlNodeType.DECLARATION'), node: isNotNull, types: contains(XmlNodeType.ELEMENT))));
    });
    test('exception during nesting, 此处应该为X', () {
      final builder = XmlBuilder();
      builder.element('outer', nest: () {
        builder.element('inner', nest: () => throw UnimplementedError());
        // expect(
        //   () => builder.element('inner', nest: () => throw UnimplementedError()),
        //   throwsUnsupportedError,
        // );
      });
      final document = builder.buildDocument();
      expect(document.toString(), '<outer/>');
    });
    test('incomplete builder, 此处应该为X', () {
      final builder = XmlBuilder();
      builder.element('element', nest: () {
        builder.buildDocument();
        // expect(builder.buildDocument, throwsStateError);
      });
      final document = builder.buildDocument();
      expect(document.toString(), '<element/>');
    });
    test('reused builder', () {
      final builder = XmlBuilder();
      builder.element('element-one');
      final firstDocument = builder.buildDocument();
      expect(firstDocument.toString(), '<element-one/>');
      builder.element('element-two');
      final secondDocument = builder.buildDocument();
      expect(secondDocument.toString(), '<element-two/>');
    });
    test('fragment builder', () {
      final builder = XmlBuilder();
      builder.element('element-one');
      builder.element('element-two');
      final xml = builder.buildFragment();
      assertFragmentInvariants(xml);
    });
  }
}
